proto3 语法

  

1、定义消息类型

  这是.proto用于定义消息类型的文件。

    syntax = "proto3"
    message SearchRequest {
        string query = 1;
        int32 page_number = 2;
        int32 result_per_page = 3;
    }

  • 该文件的第一行指定您正在使用proto3语法:如果您不这样做,协议缓冲区编译器将假定您正在使用proto2。这必须是文件的第一个非空的非注释行。

  • 所述SearchRequest消息定义指定了三个字段(名称/值对),一个用于要在此类型的消息中包含的每个数据片段。每个字段都有一个名称和类型。

  1.1 指定字段类型

  在上面的示例中,所有字段都是标量类型 :两个整数(page_numberresult_per_page)和一个字符串(query)。但是,您还可以为字段指定复合类型,包括枚举和其他消息类型。

  1.2 分配字段编号

  如您所见,消息定义中的每个字段都有唯一的编号。这些字段编号用于以消息二进制格式标识字段,并且在使用消息类型后不应更改。

  请注意,115范围内的字段编号需要一个字节进行编码,包括字段编号和字段类型。162047范围内的字段编号占用两个字节。因此,您应该为非常频繁出现的消息元素保留数字115。请记住为将来可能添加的常用元素留出一些空间。

  您可以指定的最小字段数为1,最大字段数为536,870,911。您也不能使用数字1900019999,因为它们是为Protobuf实现保留的,如果您使用其中一个保留号码,编译器会报错。同样,您不能使用任何以前保留的字段编号。

  1.3 指定字段规则

  消息字段可以是以下之一:

    • 单个字段:字段不能重复。
    • repeated:此字段可以在格式良好的消息中重复任意次数(包括零)。将保留重复值的顺序。

  在proto3中,repeated标量数字类型的字段默认使用packed编码。

  您可以在协议缓冲区编码中找到有关packed编码的更多信息。

  1.4 添加更多消息类型

  可以在单个.proto文件中定义多种消息类型。如果要定义多个相关消息,这很有用。例如,如果要定义与SearchResponse消息类型对应的回复消息格式,可以将其添加到.proto

    message SearchRequest {
        string query = 1;
        int32 page_number = 2;
        int32 result_per_page = 3;
    }

    message SearchResponse {
        ...
    }

  1.5 添加注释

  要为.proto文件添加注释,请使用///* ... */语法。

    /* SearchRequest 表示搜索查询,其中的分页选项
    * 表示要包含在响应中的结果。 */

    message SearchRequest {
        string query = 1;
        int32 page_number = 2; // 我们想要哪个页码?
        int32 result_per_page = 3; // 每页返回的结果数。
    }

  1.6 保留字段

  如果通过完全删除字段或将其注释来更新消息类型,则未来用户可以在对类型进行自己的更新时重用字段编号。如果以后加载相同的.proto旧版本,这可能会导致严重问题,包括数据损坏,隐私错误等。

  确保不会发生这种情况的一种方法是通过reserved指定已删除字段的字段编号(或名称,这也可能导致JSON序列化问题)。如果将来的任何用户尝试使用这些字段标识符,编译器将会报错。

    message Foo {
        reserved 2, 15, 9 to 11;
        reserved "foo", "bar";
    }

请注意,您不能在同一reserved语句中混合字段名称和字段编号。

  1.7 你的.proto生成什么?

  当您通过编译器protoc.exe编译.proto以后,编译器会生成您所选语言的代码,您需要使用您在文件中描述的消息类型,包括获取和设置字段值,将消息序列化为输出流,并从输入流解析您的消息。

  • 对于C#,编译器会从每个.proto文件生成一个.cs 文件,其中包含文件中描述的每种消息类型的类。

编译指令:

    protoc.exe --proto_path=./ xxx.proto --csharp_out=./

  

2、标量值类型

  标量消息字段可以具有以下类型之一,该表显示.proto文件中指定的类型,以及自动生成的类中的相应类型:

.proto类型 C#类型 说明
double double
float float
int32 int 使用可变长度编码。编码负数的效率低,如果您的字段可能有负值,请改用sint32
int64 long 使用可变长度编码。编码负数的效率低,如果您的字段可能有负值,请改用sint64
uint32 uint 使用可变长度编码。
uint64 ulong 使用可变长度编码。
sint32 int 使用可变长度编码。签名的int值。这些比常规int32更有效地编码负数。
sint64 long 使用可变长度编码。签名的int值。这些比常规int64更有效地编码负数。
fixed32 uint 总是四个字节。如果值通常大于2^28,则比uint32更有效。
fixed64 ulong 总是八个字节。如果值通常大于2^56 ,则比uint64更有效。
sfixed32 int 总是四个字节。
sfixed64 long 总是八个字节。
bool bool
string string 字符串必须始终包含UTF-8编码或7位ASCII文本。
bytes ByteString 可以包含任意字节序列。

  

3、默认值

  解析消息时,如果编码消息不包含特定的单数元素,则解析对象中的相应字段将设置为该字段的默认值。

  这些默认值是特定于类型的:

    对于字符串,默认值为空字符串。
    对于字节,默认值为空字节。
    对于`bool`,默认值为`false`。
    对于数字类型,默认值为零。
    对于枚举,默认值是第一个定义的枚举值,该值必须为`0`。
    对于消息字段,未设置该字段。它的确切值取决于语言。

  有关详细信息,请参阅生成的代码。重复字段的默认值为空(通常是相应语言的空列表)。

  

4、枚举

  在定义消息类型时,您可能希望其中一个字段只有一个预定义的值列表。例如,假设你想每个SearchRequest添加一个corpus字段,其中取值可以UNIVERSALWEBIMAGESLOCALNEWSPRODUCTSVIDEO。您可以非常简单地通过enum为每个可能的值添加一个常量来定义消息定义。

  在下面的示例中,我们添加了一个带有所有可能值的enum调用Corpus,以及一个类型的字段Corpus

    message SearchRequest {
        string query = 1;
        int32 page_number = 2;
        int32 result_per_page = 3;

        enum Corpus {
            UNIVERSAL = 0;
            WEB = 1;
            IMAGES = 2;
            LOCAL = 3;
            NEWS = 4;
            PRODUCTS = 5;
            VIDEO = 6;
        }

        Corpus corpus = 4;
    }

  如您所见,Corpus枚举的第一个常量映射为零:每个枚举定义必须包含一个映射到零的常量作为其第一个元素。这是因为:

    • 必须有一个零值,以便我们可以使用 0 作为数字默认值 。
    • 零值必须是第一个元素,以便与proto2 语义兼容,其中第一个枚举值始终是默认值。

  您可以通过为不同的枚举常量指定相同的值来定义别名。为此,您需要将allow_alias选项设置为true,否则协议编译器将在找到别名时生成错误消息。

    enum EnumAllowingAlias {
        option allow_alias = true;
        UNKNOWN = 0;
        STARTED = 1;
        RUNNING = 1;
    }

    enum EnumNotAllowingAlias {
        UNKNOWN = 0;
        STARTED = 1;
        // RUNNING = 1;  // 取消注释此行将导致 Google 内部的编译错误和外部的警告消息。
    }

  枚举器常量必须在32位整数范围内。由于enum值在online使用varint编码,因此负值效率低,因此不建议使用。

  enum也可以在外部定义,这些可以在.proto文件的任何消息定义中重用。您还可以使用enum语法将一个消息中声明的类型用作另一个消息中的字段类型:MessageType.EnumType

  在反序列化期间,将在消息中保留无法识别的枚举值,但是当反序列化消息时,如何表示这种值取决于语言。在支持具有超出指定符号范围的值的开放枚举类型的语言中,例如C++和Go,未知的枚举值仅作为其基础整数表示存储。在具有封闭枚举类型(如Java)的语言中,枚举中的大小写用于表示无法识别的值,并且可以使用特殊访问器访问基础整数。在任何一种情况下,如果消息被序列化,则仍然会使用消息序列化无法识别的值。

  4.1 保留值

  如果通过完全删除枚举条目或将其注释掉来更新枚举类型,则未来用户可以在对类型进行自己的更新时重用该数值。如果以后加载相同的.proto旧版本,这可能会导致严重问题,包括数据损坏,隐私错误等。确保不会发生这种情况的一种方法是通过reserved指定已删除条目的数值(或名称,这也可能导致JSON序列化问题)。如果将来的任何用户尝试使用这些标识符,编译器将会报错。您可以使用max关键字指定保留的数值范围达到最大可能值。

    enum Foo {
        reserved 2, 15, 9 to 11, 40 to max;
        reserved "FOO", "BAR";
    }

  请注意,您不能在同一reserved语句中混合字段名称和数值。

  

5、使用其他消息类型

  您可以使用其他消息类型作为字段类型。例如,假设你想SearchResponse消息包括Result的每个消息,要做到这一点,你可以在.proto定义一个Result消息类型,然后指定SearchResponse类型中的字段Result

    message SearchResponse {
        repeated Result results = 1;
    }

    message Result {
        string url = 1;
        string title = 2;
        repeated string snippets = 3;
    }

  5.1 导入定义

  在上面的示例中,Result消息类型和SearchResponse在同一文件中定义,如果要用作字段类型的消息类型已在另一个.proto文件中定义,该怎么办?

  您可以通过导入来使用其他.proto文件中的定义。要导入其他.proto的定义,请在文件顶部添加import语句:

    import "myproject/other_protos.proto";

  默认情况下,您只能使用直接导入.proto文件中的定义。但是,有时您可能需要将.proto文件移动到新位置。您可以在旧位置放置一个虚拟.proto文件,以使用该import public概念将所有导入转发到新位置,而不是直接移动文件并在一次更改中更新所有调用站点。任何导入包含该import public语句的proto都可以传递依赖关系。例如:

    // new.proto
    // 所有定义都移到这里

    // old.proto
    // 这是所有客户端都要导入的原型。
    import public "new.proto";
    import "other.proto";

    // client.proto
    import "old.proto";
    // 您使用old.proto和new.proto中的定义,但不使用other.proto

  5.2 使用proto2消息类型

  可以导入proto2消息类型并在proto3消息中使用它们,反之亦然。但是,proto2枚举不能直接用于proto3语法(如果导入的proto2消息使用它们就可以了)。

  

6、嵌套类型

  您可以在其他消息类型中定义和使用消息类型,如下例所示:此处 Result消息在SearchResponse消息中定义:

    message SearchResponse {
        message Result {
            string url = 1;
            string title = 2;
            repeated string snippets = 3;
        }

        repeated Result results = 1;
    }

  如果要在其父消息类型之外重用此消息类型,请将其称为:Parent.Type

    message SomeOtherMessage {
        SearchResponse.Result result = 1;
    }

  您可以根据需要深入嵌套消息:

    message Outer {            // Level 0
        message MiddleAA {     // Level 1
            message Inner {    // Level 2
                int64 ival = 1;
                bool  booly = 2;
            }
        }
        message MiddleBB {    // Level 1
            message Inner {   // Level 2
                int32 ival = 1;
                bool  booly = 2;
            }
        }
    }

  

7、更新消息类型

  如果现有的消息类型不再满足您的所有需求 - 例如,您希望消息格式具有额外的字段 - 但您仍然希望使用使用旧格式创建的代码,请不要担心!在不破坏任何现有代码的情况下更新消息类型非常简单。请记住以下规则:

    • 请勿更改任何现有字段的字段编号。

    • 如果添加新字段,则使用 “旧” 消息格式按代码序列化的任何消息仍可由新生成的代码进行解析。您应该记住这些元素的默认值,以便新代码可以正确地与旧代码生成的消息进行交互。同样,您的新代码创建的消息可以由旧代码解析:旧的二进制文件在解析时只是忽略新字段。有关详细信息,请参阅 “ 未知字段 ” 部分

    • 只要在更新的消息类型中不再使用字段编号,就可以删除字段。您可能希望重命名该字段,可能添加前缀 “OBSOLETE_” ,或者保留字段编号,以便您的未来用户不会意外地重复使用该编号。

    • int32uint32int64uint64,和bool都是兼容的 - 这意味着你可以改变这些类型向前或向后兼容。如果从文件中解析出一个不符合相应类型的数字,您将获得与在 C ++ 中将该数字转换为该类型相同的效果(例如,如果将 64 位数字作为 int32 读取,它将被截断为 32 位)。

    • sint32sint64彼此兼容但与其他整数类型不兼容。

    • stringbytes,只要字节是有效的UTF-8,它们是兼容的。

    • bytes 如果字节包含消息的编码版本,则嵌入消息是兼容的。

    • fixed32兼容sfixed32,并且fixed64兼容sfixed64

    • enum兼容int32uint32int64,和 uint64 (注意,如果他们不适合的值将被截断)。但请注意,在反序列化消息时,客户端代码可能会以不同方式对待它们:例如,enum将在消息中保留未识别的proto3 类型,但在反序列化消息时如何表示这种类型取决于语言。int字段总是保留它们的值。

    • 将单个值更改为新成员oneof是安全且二进制兼容的。如果您确定没有代码一次设置多个字段,则将多个字段移动到新字段可能是安全的。将任何字段移动到现有字段oneof并不安全。

  

8、未知字段

  未知字段是格式良好的协议缓冲区序列化数据,表示解析器无法识别的字段。例如,当旧二进制文件解析具有新字段的新二进制文件发送的数据时,这些新字段将成为旧二进制文件中的未知字段。

  最初,proto3消息在解析期间总是丢弃未知字段,但在 3.5 版本中,我们重新引入了保存未知字段以匹配proto2行为。在版本 3.5 及更高版本中,未知字段在解析期间保留并包含在序列化输出中。

  

9、Any消息类型

  该Any消息类型,可以使用网址作为嵌入式类型,而不必自己.proto 定义。一个Any含有任意的序列化消息bytes,以充当一个全局唯一标识符和解析到该消息的类型的URL一起。要使用该Any类型,您需要导入google/protobuf/any.proto

    import "google/protobuf/any.proto";

    message ErrorStatus {
        string message = 1;
        repeated google.protobuf.Any details = 2;
    }

  给定消息类型的默认类型URL是type.googleapis.com/packagename.messagename

  

10、Oneof

  如果您有一个包含许多字段的消息,并且最多只能同时设置一个字段,则可以使用oneof功能强制执行此行为并节省内存。   除了一个共享内存中的所有字段之外,其中一个字段类似于常规字段,并且最多可以同时设置一个字段。设置oneof的任何成员会自动清除所有其他成员。您可以使用特殊case()WhichOneof()方法检查oneof中的哪个值(如果有),具体取决于您选择的语言。

  10.1 使用Oneof

  要在您 .proto 中定义 oneof ,请使用 oneof 关键字后跟您的 oneof 名称,在这种情况下 test_oneof

    message SampleMessage {
        oneof test_oneof {
            string name = 4;
            SubMessage sub_message = 9;
        }
    }

  然后,将oneof字段添加到oneof定义中。您可以添加任何类型的字段,但不能使用repeated字段。

  10.2 Oneof特点

  • 设置oneof字段将自动清除oneof的所有其他成员。因此,如果您设置了多个字段,则只有您设置的最后一个字段仍然具有值。

  • 如果解析器遇到同一个oneof的多个成员,则在解析的消息中仅使用看到的最后一个成员。

  • 字段不能使用repeated

  • Reflection API 适用于其中一个字段。

  10.3 向后兼容问题

  添加或删除其中一个字段时要小心。如果检查oneof返回的值None/NOT_SET,这可能意味着oneof尚未设置或已在不同版本的oneof的被设置为一个字段。没有办法区分,因为没有办法知道线上的未知字段是否是其中一个成员。

  标签重用问题

  • 将字段移入或移出oneof:在序列化和解析消息后,您可能会丢失一些信息(某些字段将被清除)。但是,您可以安全地将单个字段移动到新的 oneof中,并且如果已知只有一个字段被设置,则可以移动多个字段。

  • 删除oneof字段并将其添加回:在序列化和解析消息后,这可能会清除当前设置的oneof字段。

  • 拆分或合并oneof:这与移动常规字段有类似的问题。

  

11、映射

  如果要在数据定义中创建关联映射,protobuf提供了一种方便的快捷方式语法:

    map<key_type,value_type> map_field = N;

  ... 其中key_type可以是任何整数或字符串类型(因此,除了浮点类型之外的任何标量、bytes类型)。请注意,枚举不是有效的 key_typevalue_type可以是任何类型的除另一映射。

  因此,例如,如果要创建项目映射,其中每条Project消息都与字符串键相关联,则可以像下面这样定义它:

    map<string,Project> projects = 3;

  • 映射字段不能repeated

  • 映射值的有线格式排序和映射迭代排序未定义,因此您不能依赖于特定顺序的映射项目。

  • 为.proto生成文本格式时,映射按键排序。数字键按数字排序。

  • 解析或合并时,如果有重复的映射键,则使用最后看到的键。从文本格式解析映射时,如果存在重复键,则解析可能会失败。

  • 如果为映射字段提供键但没有值,则字段序列化时的行为取决于语言。在C++JavaPython中,类型的默认值是序列化的,而在其他语言中没有任何序列化。

  11.1 向后兼容性

  映射语法在线上等效于以下内容,因此不支持映射的Protobuf实现仍可处理您的数据:

    message MapFieldEntry {
        key_type key = 1;
        value_type value = 2;
    }

    repeated MapFieldEntry map_field = N;

  任何支持映射的Protobuf实现都必须生成和接受上述定义可以接受的数据

  

12、包

  您的package可以向.proto文件添加可选说明符,以防止协议消息类型之间的名称冲突。

    package foo.bar;
    message Open { ... }

  然后,您可以在定义消息类型的字段时使用包说明符:

    message Foo {
        ...
        foo.bar.Open open = 1;
        ...
    }

  包说明符影响生成的代码的方式取决于您选择的语言:

    • 在C#中,包转换为PascalCase后用作命名空间,除非您在.proto文件中明确提供option csharp_namespace。例如,Open将在命名空间Foo.Bar中。

  12.1 包和名称解析

  Protobuf语言中的类型名称解析与C++类似:首先搜索最里面的范围,然后搜索下一个范围,依此类推,每个包被认为是其父包的“内部”。一个领先的'.'(例如,.foo.bar.Baz)意味着从最外层的范围开始。

  Protobuf编译器通过解析导入的.proto文件来解析所有类型名称。每种语言的代码生成器都知道如何使用该语言引用每种类型,即使它具有不同的范围规则。

  

13、定义服务

  如果要将消息类型与RPC(远程过程调用)系统一起使用,则可以在 .proto文件中定义RPC服务接口,Protobuf编译器将使用您选择的语言生成服务接口代码和存根。因此,例如,如果要使用带有SearchRequest和返回的SearchResponse方法定义RPC服务,可以按如下方式在.proto文件中定义它:

    service SearchService {
        rpc Search (SearchRequest) returns (SearchResponse);
    }

  与Protobuf一起使用的最简单的RPC系统是gRPC:一种在Google开发的语言和平台中立的开源RPC系统。gRPC特别适用于Protobuf,并允许您使用特殊的Protobuf编译器插件直接从您的.proto文件生成相关的RPC代码。

  

14、JSON映射

  Proto3支持JSON中的规范编码,使得在系统之间共享数据变得更加容易。在下表中逐个类型地描述编码。

  如果JSON编码数据中缺少值,或者其值为null,则在解析为Protobuf时,它将被解释为适当的默认值。如果字段在Protobuf中具有默认值,则默认情况下将在JSON编码数据中省略该字段以节省空间。实现可以提供用于在JSON编码的输出中发出具有默认值的字段的选项。

proto3 JSON JSON example Notes
message object {"fooBar":v, "g":null,...} 生成JSON对象。消息字段名称映射到lowerCamelCase并成为JSON对象键。如果json_name指定了field选项,则指定的值将用作键。解析器接受lowerCamelCase名称(或json_name选项指定的名称)和原始proto字段名称。null是所有字段类型的可接受值,并将其视为相应字段类型的默认值。
enum string "FOO_BAR" 使用proto中指定的枚举值的名称。解析器接受枚举名称和整数值。
map<K,V> object {"k":v, ...} 所有键都转换为字符串。
repeated V array [v, ...] null被接受为空列表[]。
bool true, false true,false
string string "hello world"
bytes base64 string "YWJjMTIzIT8+" JSON值将是使用带填充的标准base64编码编码为字符串的数据。接受带有/不带填充的标准或URL安全base64编码。
int32,fixed32,uint32 number 1,-10,0 JSON值将是十进制数。接受数字或字符串。
int64,fixed64,uint64 string "1", "-10" JSON值将是十进制字符串。接受数字或字符串。
float, double number 1.1, -10.0, 0, "Nan", "Infinity" JSON值将是一个数字或一个特殊字符串值“NaN”,“Infinity”和“-Infinity”。接受数字或字符串。指数表示法也被接受。
Any object {"@type":"url" "f": v, ...} 如果Any包含具有特殊JSON 映射的值,则将按如下方式进行转换:。否则,该值将转换为JSON对象,并将插入该字段以指示实际的数据类型。{"@type": xxx, "value": yyy}"@type"
Timestamp string "1972-01-01T10:00:20.021Z" 使用RFC3339,其中生成的输出将始终被Z标准化并使用0,3,6或9个小数位。也接受“Z”以外的偏移。
Duration string "1.000340012s", "1s" 生成的输出始终包含0,3,69个小数位,具体取决于所需的精度,后跟后缀“s”。接受的是任何小数位(也没有),只要它们符合纳秒精度并且后缀“s”是必需的。
Struct object { ... } 任何JSON对象。见struct.proto
Wrapper types various types 2,"2","foo",true,"true",null,0,... 包装器在JSON中使用与包装基元类型相同的表示形式,除了null在数据转换和传输期间允许和保留的表示形式。
FieldMask string "f.fooBar, h" field_mask.proto
ListValue value [foo, bar, ...]
Value value 任何JSON值
NullValue null JSON null

  14.1 JSON选项

  proto3 JSON 实现可以提供以下选项:

    • 使用默认值发出字段:默认情况下,proto3 JSON输出中省略了具有默认值的字段。实现可以提供覆盖此行为的选项,并使用其默认值输出字段。

    • 忽略未知字段:默认情况下,proto3 JSON解析器应拒绝未知字段,但可以提供忽略解析中未知字段的选项。

    • 使用proto字段名称而不是lowerCamelCase名称:默认情况下,proto3 JSON打印机应将字段名称转换为lowerCamelCase并将其用作JSON名称。实现可以提供使用proto字段名称作为JSON名称的选项。proto3 JSON解析器需要接受转换后的lowerCamelCase名称和proto字段名称。

    • 将枚举值发送为整数而不是字符串:默认情况下,在JSON输出中使用枚举值的名称。可以提供选项以使用枚举值的数值。

  

15、选项

  .proto文件中的各个声明可以使用许多选项进行注释。选项不会更改声明的整体含义,但可能会影响在特定上下文中处理它的方式。可用选项的完整列表在google/protobuf/descriptor.proto中定义。

  一些选项是文件级选项,这意味着它们应该在顶级范围内编写,而不是在任何消息,枚举或服务定义中。一些选项是消息级选项,这意味着它们应该写在消息定义中。一些选项是字段级选项,这意味着它们应该写在字段定义中。选项也可以写在枚举类型,枚举值,服务类型和服务方法上;但是,目前没有任何有用的选择。

  15.1 自定义选项

  Protocol Buffers还允许您定义和使用自己的选项。这是大多数人不需要的高级功能。如果您确实认为需要创建自己的选项,请参阅Proto2语言指南 以获取详细信息。请注意,创建自定义选项使用的扩展名仅允许用于proto3中的自定义选项。

  

16、生成您的类

  要生成你需要在定义的消息类型的工作使用 Java, Python, C++, Go, Ruby, Objective-C, or C# 代码 .proto 文件,你需要运行Protobuf编译器protoc 编译.proto 。如果尚未安装编译器,请下载该软件包 并按照自述文件中的说明进行操作。

    protoc --proto_path=IMPORT_PATH --csharp_out=DST_DIR path/to/file.proto

  • IMPORT_PATH指定.proto解析import指令时在其中查找文件的目录。如果省略,则使用当前目录。可以通过 --proto_path 多次传递选项来指定多个导入目录;他们将按顺序搜索。可以用作简短的形式:-I=IMPORT_PATH --proto_path

  • 为了方便起见,如果DST_DIR结束于.zip.jar,编译器会将输出写入具有给定名称的单个ZIP格式存档文件。.jar输出还将根据Java JAR规范的要求提供清单文件。请注意,如果输出存档已存在,则会被覆盖;编译器不够智能,无法将文件添加到现有存档中。

  • 您必须提供一个或多个.proto文件作为输入。.proto可以一次指定多个文件。虽然文件是相对于当前目录命名的,但每个文件必须位于其中一个文件中,IMPORT_PATH以便编译器可以确定其规范名称。

17、解析和序列化

  使用协议缓冲区的全部目的是序列化您的数据,以便可以在其他地方解析它。每个生成的类都有一个WriteTo(CodedOutputStream)方法,其中CodedOutputStream是协议缓冲区运行时库中的一个类。但是,通常您将使用其中一种扩展方法写入常规System.IO.Stream或将消息转换为字节数组或ByteString。这些扩展消息在Google.Protobuf.MessageExtensions类中,因此当您想要序列化时,通常需要命名空间的using指令Google.Protobuf。例如:

    using Google.Protobuf;
    ...
    Person john = ...; // Code as before
    using (var output = File.Create("john.dat"))
    {
        john.WriteTo(output);
    }

解析也很简单。每个生成的类都有一个静态Parser属性,该属性返回该类型的MessageParser<T>对象。这反过来又有解析流,字节数组和ByteString的方法。因此,要解析我们刚刚创建的文件,我们可以使用:

    Person john;
    using (var input = File.OpenRead("john.dat"))
    {
        john = Person.Parser.ParseFrom(input);
    }

🔚